//	--------------------------------------------------------------------
//	Copyright(C) 2006,2007 Zhao Yukun. All rights reserved.
//	This file is a part of the Xeres system.
//	Xeres 2007
//	Contact : xeres.engine@gmail.com
//	$(reservedInfo)
//	$(reservedDeclaration)
//	--------------------------------------------------------------------
///	@file	<xml_parser.cpp>
///	@path	~/src/database/xres/
///	@date	2007/12/04
///	@desc	Encapsulates a simple XML parser.

#include "config/config.h"

#include "database/xres/xml_parser.h"

TRACE_CATEGORY( _S("Database/xres/xml_parser") , TRACE_LV_INFO );

// Microsoft's UTF-8 spec of leading byte
static const uchar MSXML_UTF_LEAD_0 	= 0xefU;
static const uchar MSXML_UTF_LEAD_1 	= 0xbbU;
static const uchar MSXML_UTF_LEAD_2 	= 0xbfU;

static const wchar_t * UTF8_DECL		= _S("UTF-8");

// - Elements start with a letter or underscore, but xml is reserved.
// - Comments: <!--
// - Declaration: <?xml
// - Entity: <!
// - CDATA: <![CDATA[
static const char * XML_HEADER			= "<?xml";
static const char * COMMENT_HEADER		= "<!--";
static const char * COMMENT_TAIL		= "-->";
static const char * DTD_HEADER			= "<!";
static const char * CDATA_HEADER		= "<![CDATA[";
static const char * CDATA_TAIL			= "]]>";

static const char * SINGLE_QUOTE		= "\'";
static const char * DOUBLE_QUOTE		= "\"";

static const char * CHAR_ENTITIES [][2]	= {
	{ "&amp;"	, "&" } ,
	{ "&lt;"	, "<" } ,
	{ "&gt;"	, ">" } ,
	{ "&quot;"  , "\"" } ,
	{ "&apos;"  , "\'" } ,
	{ NULL , NULL }
};

namespace xeres {

	// XmlParser
	XmlParser::XmlParser( void )
	{
	}

	// ~XmlParser
	XmlParser::~XmlParser( void )
	{
	}

	static inline bool IsWhiteSpace( char c )		
	{ 
		return ( isspace( (unsigned char) c ) || c == '\n' || c == '\r' ); 
	}

	// SkipWhitespace
	template< typename _Pointer > void XmlParser::SkipWhitespace( _Pointer& ptr )
	{
		if( m_encoding == XML_ENCODING_UTF8 )
		{
			while( *ptr )
			{
				// Skip the stupid Microsoft UTF-8 Byte order marks
				if(	ptr[0] == MSXML_UTF_LEAD_0 &&
					ptr[1] == MSXML_UTF_LEAD_1 &&
					ptr[2] == MSXML_UTF_LEAD_2 )
				{
					ptr += 3;
					continue;
				}
				else if(
					ptr[0] == MSXML_UTF_LEAD_0 &&
					ptr[1] == 0xbfU &&
					ptr[2] == 0xbeU )
				{
					ptr += 3;
					continue;
				}
				else if(
					ptr[0] == MSXML_UTF_LEAD_0 &&
					ptr[1] == 0xbfU &&
					ptr[2] == 0xbfU )
				{
					ptr += 3;
					continue;
				}

				// Still using old rules for white space.
				if( IsWhiteSpace( *ptr ) )
					++ptr;
				else
					break;
			}
		}
		else
		{
			while( ( *ptr ) && IsWhiteSpace( *ptr ) )
				++ptr;
		}
	}

	// convert char to lower case char
	static inline int ToLower( int v , XmlEncoding encoding )
	{
		if( encoding == XML_ENCODING_UTF8 )
		{
			if ( v < 128 )
				return tolower( v );
			return v;
		}
		else
		{
			return tolower( v );
		}
	}

	static inline int IsAlpha( unsigned char anyByte )
	{
		if( anyByte < 127 )
			return isalpha( anyByte );
		else
			return 1;
	}

	static inline int IsAlphaNum( unsigned char anyByte )
	{
		if( anyByte < 127 )
			return isalnum( anyByte );
		else
			return 1;
	}

	// string compare
	template< typename _Pointer > static inline bool StringCompare(
		const char * str , _Pointer& data , bool ignoreCase , XmlEncoding encoding )
	{
		if( ignoreCase )
		{
			for( uchar i = 0 ; str[i] ; ++i )
			{
				if( ToLower( str[i] , encoding ) != ToLower( data[i] , encoding ) )
					return false;
			}
		}
		else
		{
			for( uchar i = 0 ; str[i] ; ++i )
			{
				if( str[i] != data[i] )
					return false;
			}
		}
		return true;
	}

	// Identify
	template< typename _Pointer > XmlParser::NodeType XmlParser::Identify( _Pointer& ptr )
	{
		NodeType retval = NODE_ERROR;

		// skip opening white space
		SkipWhitespace( ptr );
		if( ( *ptr == 0 ) || ( *ptr != '<' ) )
		{
			return NODE_ERROR;
		}

		if( StringCompare( XML_HEADER , ptr , true , m_encoding ) )
		{
			TRACE_DEBUG( _S("XmlParser - Parsing declaration.") );
			retval = NODE_DECLARATION;
		}
		else if( StringCompare( COMMENT_HEADER , ptr , false , m_encoding ) )
		{
			TRACE_DEBUG( _S("XmlParser - Parsing comment.") );
			retval = NODE_COMMENT;
		}
		else if( StringCompare( CDATA_HEADER , ptr , false , m_encoding ) )
		{
			TRACE_DEBUG( _S("XmlParser - Parsing CDATA.") );
			retval = NODE_CDATA;
		}
		else if( StringCompare( DTD_HEADER , ptr , false , m_encoding ) )
		{
			TRACE_DEBUG( _S("XmlParser - Parsing DTD.") );
			TRACE_ERROR( _S("XmlParser::Identify: Currently DTD is not supported.") );
		}
		else if( IsAlpha( ptr[1] ) || ptr[1] == '_' )
		{
			TRACE_DEBUG( _S("XmlParser - Parsing Element.") );
			retval = NODE_ELEMENT;
		}

		// FIXME: should handle unknown nodes
		return retval;
	}

	// ReadIdentifier
	template< typename _Pointer > bool XmlParser::ReadIdentifier( _Pointer& ptr , WString& name )
	{
		name.clear();

		// skip opening white space
		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ReadIdentifier: Unexpected end of the document.") );
			return false;
		}

		// parse identifier
		while( *ptr && ( IsAlphaNum( *ptr ) || 
			*ptr == '_' ||
			*ptr == '-' ||
			*ptr == '.' ||
			*ptr == ':' ) )
		{
			byte src[4] = { ptr[0] , ptr[1] , ptr[2] , 0 };
			size_t len = decode_from_utf8( src , 1 , name );
			ptr += (uchar)len;
		}
		return true;
	}

	// ReadText
	template< typename _Pointer > bool XmlParser::ReadText( _Pointer& ptr , WString& text ,
		const char * endTag , bool trimWhiteSpace , bool caseInsensitive , bool eatEndTag )
	{
		text.clear();

		if( m_encoding == XML_ENCODING_UTF8 )
		{
			if( !trimWhiteSpace )
			{
				while( *ptr && ( !StringCompare( endTag , ptr , caseInsensitive , m_encoding ) ) )
				{
					if( ptr[0] == '&' )
					{
						bool found = false;
						for( int i = 0 ; CHAR_ENTITIES[i][0] != NULL ; ++i )
						{
							if( StringCompare( CHAR_ENTITIES[i][0] , ptr , caseInsensitive , m_encoding ) )
							{
								text.push_back( CHAR_ENTITIES[i][1][0] );
								ptr += strlen( CHAR_ENTITIES[i][0] );
							}
						}
						if( found )
							continue;
					}
					byte src[4] = { ptr[0] , ptr[1] , ptr[2] , 0 };
					size_t len = decode_from_utf8( src , 1 , text );
					ptr += (uchar)len;
				}
			}
			else
			{
				bool whiteSpace = false;
				// skip
				SkipWhitespace( ptr );
				while( *ptr && ( !StringCompare( endTag , ptr , caseInsensitive , m_encoding ) ) )
				{
					if( IsWhiteSpace( *ptr ) )
					{
						whiteSpace = true;
						++ptr;
					}
					else
					{
						if( whiteSpace )
						{
							text.push_back( ' ' );
							whiteSpace = false;
						}
						if( ptr[0] == '&' )
						{
							bool found = false;
							for( int i = 0 ; CHAR_ENTITIES[i][0] != NULL ; ++i )
							{
								if( StringCompare( CHAR_ENTITIES[i][0] , ptr , caseInsensitive , m_encoding ) )
								{
									text.push_back( CHAR_ENTITIES[i][1][0] );
									ptr += strlen( CHAR_ENTITIES[i][0] );
								}
							}
							if( found )
								continue;
						}
						byte src[4] = { ptr[0] , ptr[1] , ptr[2] , 0 };
						size_t len = decode_from_utf8( src , 1 , text );
						ptr += (uchar)len;
					}
				}
			}
		}
		else // composite mbs
		{
			AString astr;
			if( !trimWhiteSpace )
			{
				while( *ptr && ( !StringCompare( endTag , ptr , caseInsensitive , m_encoding ) ) )
				{
					if( ptr[0] == '&' )
					{
						bool found = false;
						for( int i = 0 ; CHAR_ENTITIES[i][0] != NULL ; ++i )
						{
							if( StringCompare( CHAR_ENTITIES[i][0] , ptr , caseInsensitive , m_encoding ) )
							{
								astr.push_back( CHAR_ENTITIES[i][1][0] );
								ptr += strlen( CHAR_ENTITIES[i][0] );
							}
						}
						if( found )
							continue;
					}
					astr.push_back( *ptr );
					++ptr;
				}
			}
			else
			{
				bool whiteSpace = false;
				// skip
				SkipWhitespace( ptr );
				while( *ptr && ( !StringCompare( endTag , ptr , caseInsensitive , m_encoding ) ) )
				{
					if( IsWhiteSpace( *ptr ) )
					{
						whiteSpace = true;
						++ptr;
					}
					else
					{
						if( whiteSpace )
						{
							astr.push_back( ' ' );
							whiteSpace = false;
						}
						if( ptr[0] == '&' )
						{
							bool found = false;
							for( int i = 0 ; CHAR_ENTITIES[i][0] != NULL ; ++i )
							{
								if( StringCompare( CHAR_ENTITIES[i][0] , ptr , caseInsensitive , m_encoding ) )
								{
									astr.push_back( CHAR_ENTITIES[i][1][0] );
									ptr += strlen( CHAR_ENTITIES[i][0] );
								}
							}
							if( found )
								continue;
						}
						astr.push_back( *ptr );
						++ptr;
					}
				}
			}
			// convert
			mbs_to_wcs( astr , text );
		}
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ReadText: Unexpected end of the document.") );
			return false;
		}
		if( eatEndTag )
			ptr += (uchar)strlen( endTag );
		return true;
	}

	// ReadValue
	template< typename _Pointer > bool XmlParser::ReadValue( _Pointer& ptr , WString& value )
	{
		value.clear();

		// skip white space
		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ReadValue: Unexpected end of the document.") );
			return false;
		}

		while( *ptr )
		{
			if ( *ptr != '<' )
			{
				// text element
				if( !ParseText( ptr , value ) )
					return false;

				// add text child
			} 
			else 
			{
				// We hit a '<'
				// Have we hit a new element or an end tag? This could also be
				// a TiXmlText in the "CDATA" style.
				if( StringCompare( "</" , ptr , false , m_encoding ) )
				{
					return true;
				}
				else
				{
					switch( Identify( ptr ) )
					{
					case NODE_DECLARATION:
						//if( ! ParseDeclaration( ptr ) )
						//	return false;
						TRACE_ERROR( _S("XmlParser::ReadValue: Invalid declaration inside element.\n") );
						return false;

					case NODE_COMMENT:
						if( ! ParseComment( ptr ) )
							return false;
						break;

					case NODE_ELEMENT:
						if( ! ParseElement( ptr ) )
							return false;
						break;

					case NODE_CDATA:
						{
							std::vector<byte> cdata;
							if( ! ParseCDATA( ptr , cdata ) )
								return false;
							decode_from_utf8( &cdata.front() , cdata.size() , value );
						}
						break;
					default:
						return false;

					}
				}
			}
			SkipWhitespace( ptr );
		}

		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ReadValue: Unexpected end of the document.") );
			return false;
		}
		return true;
	}

	// ParseCDATA
	template< typename _Pointer > bool XmlParser::ParseCDATA( _Pointer& ptr , std::vector<byte>& cdata )
	{
		// must ensure CDATA header
		assert( StringCompare( CDATA_HEADER , ptr , false , m_encoding ) );

		ptr += strlen( CDATA_HEADER );
		// get all CDATA block
		while( *ptr && !StringCompare( CDATA_TAIL , ptr , false , m_encoding ) )
		{
			cdata.push_back( *ptr );
			++ptr;
		}

		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseCDATA: Unexpected end of the document.") );
			return false;
		}

		ptr += strlen( CDATA_TAIL );

		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseCDATA: Unexpected end of the document.") );
			return false;
		}

		return true;
	}

	// ParseText
	template< typename _Pointer > bool XmlParser::ParseText( _Pointer& ptr , WString& text )
	{
		text.clear();

		if( StringCompare( CDATA_HEADER , ptr , false , m_encoding ) )
		{
			std::vector<byte> cdata;

			if( ! ParseCDATA( ptr , cdata ) )
				return false;

			decode_from_utf8( &cdata.front() , cdata.size() , text );
		}
		else
		{
			if( ! ReadText( ptr , text , "<" , true , false , false ) )
				return false;
		}
		return true;
	}

	// ParseComment
	template< typename _Pointer > bool XmlParser::ParseComment( _Pointer& ptr )
	{
		// must ensure declaration before calling this function
		assert( StringCompare( COMMENT_HEADER , ptr , true , m_encoding ) );

		ptr += (uchar)strlen( COMMENT_HEADER );

		// FIXME! skip ? or note it down?
		while( *ptr && !StringCompare( COMMENT_TAIL , ptr , true , m_encoding ) )
		{
			++ptr;
		}

		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ReadText: Unexpected end of the document.") );
			return false;
		}

		ptr += strlen( COMMENT_TAIL );
		return true;
	}

	// ParseAttribute
	template< typename _Pointer > bool XmlParser::ParseAttribute(
		_Pointer& ptr , WString& attr , WString& data )
	{
		// read identifier
		ReadIdentifier( ptr , attr );

		// skip white space
		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseAttribute: Unexpected end of the document.") );
			return false;
		}

		if( *ptr != '=' )
		{
			TRACE_ERROR( _S("XmlParser::ParseAttribute: Expecting '=' after attribute identifier.") );
			return false;
		}
		++ptr;

		// skip white space
		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseAttribute: Unexpected end of the document.") );
			return false;
		}

		// read value

		if( *ptr == '\'' )
		{
			++ptr;
			return ReadText( ptr , data , SINGLE_QUOTE , false , false , true );
		}
		else if( *ptr == '\"' )
		{
			++ptr;
			return ReadText( ptr , data , DOUBLE_QUOTE , false , false , true );
		}
		else
		{
			// All attribute values should be in single or double quotes.
			// But this is such a common error that the parser will try
			// its best, even without them.
			data.clear();
			while( *ptr && !IsWhiteSpace( *ptr ) && *ptr != '/' && *ptr != '>' )
			{
				if ( *ptr == '\'' || *ptr == '\"' )
				{
					// [ 1451649 ] Attribute values with trailing quotes not handled correctly
					// We did not have an opening quote but seem to have a 
					// closing one. Give up and throw an error.
					TRACE_ERROR( _S("XmlParser::ParseAttribute: Invalid character in attribute.\n") );
					return false;
				}
				data.push_back( *ptr );
				++ptr;
			}
			return true;
		}
	}

	// ParseElement
	template< typename _Pointer > bool XmlParser::ParseElement( _Pointer& ptr )
	{
		// ensure element start
		assert( '<' == *ptr );
		++ptr;

		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseElement: Unexpected end of the document.") );
			return false;
		}

		WString name;
		if( !ReadIdentifier( ptr , name ) )
			return false;

		static const char * END_TAG_HEADER	= "</";
		static const char * END_TAG_TAIL	= ">";

		if( !OnElementBegin( name ) )
			return false;

		while( *ptr )
		{
			SkipWhitespace( ptr );
			if( *ptr == NULL )
			{
				TRACE_ERROR( _S("XmlParser::ParseElement: Unexpected end of the document.") );
				return false;
			}

			if( *ptr == '/' )
			{
				++ptr;
				if( *ptr != '>' )
				{
					TRACE_ERROR( _S("XmlParser::ParseElement: Expecting '>' after '/'.") );
					return false;
				}
				++ptr;

				// empty tag

				break;
			}
			else if( *ptr == '>' )
			{
				++ptr;

				WString value;

				// Done with attributes (if there were any.)
				// Read the value -- which can include other
				// elements -- read the end tag, and return.
				if( !ReadValue( ptr , value ) )
					return false;

				OnValue( value );

				// We should find the end tag now
				if( StringCompare( END_TAG_HEADER , ptr , false , m_encoding ) )
				{
					ptr += strlen( END_TAG_HEADER );

					WString nn;
					if( ReadIdentifier( ptr , nn ) && nn == name )
					{
						if( StringCompare( END_TAG_TAIL , ptr , false , m_encoding ) )
						{
							ptr += strlen( END_TAG_TAIL );
							break;
						}
					}
				}
				TRACE_ERROR( _S("XmlParser::ParseElement: Missing end of element tag.") );
				return false;
			}
			else
			{
				// Try to read an attribute:

				WString attr , data;
				if( !ParseAttribute( ptr , attr , data ) )
					return false;

				// call attribute callback
				if( !OnAttribute( attr , data ) )
					return false;
			}
		}

		if( ! OnElementEnd() )
			return false;

		return true;		
	}

	// ParseDeclaration
	template< typename _Pointer > bool XmlParser::ParseDeclaration( _Pointer& ptr )
	{
		// must ensure declaration before calling this function
		assert( StringCompare( XML_HEADER , ptr , true , m_encoding ) );

		ptr += (uchar)strlen(XML_HEADER);
		while( *ptr )
		{
			if( *ptr == '>' )
			{
				// end
				++ptr;
				return true;
			}
			if( *ptr == '?' )
			{
				++ptr;
				if( *ptr == '>' )
				{
					++ptr;
					return true;
				}
				TRACE_ERROR( _S("XmlParser::ParseDeclaration: Syntax error, missing '>' after '?'.") );
				return false;
			}

			SkipWhitespace( ptr );
			if( *ptr == NULL )
			{
				TRACE_ERROR( _S("XmlParser::ParseDeclaration: Unexpected end of the document.") );
				return false;
			}

			if( StringCompare( "version" , ptr , true , m_encoding ) )
			{
				// version
				WString name;
				WString data;
				if( !ParseAttribute( ptr , name , data  ) )
					return false;
			}
			else if( StringCompare( "encoding" , ptr , true , m_encoding ) )
			{
				// encoding
				WString name;
				WString data;
				if( !ParseAttribute( ptr , name , data  ) )
					return false;

				if( _wcsicmp( data.c_str() , UTF8_DECL ) == 0 )
				{
					m_encoding = XML_ENCODING_UTF8;
				}
				else
				{
					TRACE_ERROR( _S("XmlParser::ParseDeclaration: Only UTF-8 encoding is valid.") );
				}
			}
			else if( StringCompare( "standalone" , ptr , true , m_encoding ) )
			{
				// standalone
				WString name;
				WString data;
				if( !ParseAttribute( ptr , name , data  ) )
					return false;
			}
			else
			{
				// Unknown declaration
				TRACE_ERROR( _S("XmlParser::ParseDeclaration: Unknown declaration.\n") );
				return false;
			}
			// skip white space
			SkipWhitespace( ptr );
			if( *ptr == NULL )
			{
				TRACE_ERROR( _S("XmlParser::ParseDeclaration: Unexpected end of the document.") );
				return false;
			}
		}
		return true;
	}

	// ParseDocument
	template< typename _Pointer > bool XmlParser::ParseDocument( _Pointer& ptr )
	{
		// first we check for the Microsoft UTF-8 leading bytes
		if( ptr[0] == MSXML_UTF_LEAD_0 &&
			ptr[1] == MSXML_UTF_LEAD_1 &&
			ptr[2] == MSXML_UTF_LEAD_2 )
		{
			m_encoding = XML_ENCODING_UTF8;
			ptr += 3;
		}
		else
		{
			m_encoding = XML_ENCODING_UNKNOWN;
		}
		
		// skip opening white space
		SkipWhitespace( ptr );
		if( *ptr == NULL )
		{
			TRACE_ERROR( _S("XmlParser::ParseDocument: Unexpected end of the document.") );
			return false;
		}

		while( *ptr )
		{
			// identify which type of node
			NodeType type = Identify( ptr );

			switch( type )
			{
			case NODE_DECLARATION:
				if( ! ParseDeclaration( ptr ) )
					return false;
				break;

			case NODE_COMMENT:
				if( ! ParseComment( ptr ) )
					return false;
				break;

			case NODE_ELEMENT:
				if( ! ParseElement( ptr ) )
					return false;
				break;

			case NODE_CDATA:
				TRACE_ERROR( _S("XmlParser::ParseDocument: Unexpected global CDATA section.") );
				return false;

			default:
				TRACE_ERROR( _S("XmlParser::Syntax error.") );
				return false;
			}

			SkipWhitespace( ptr );
			if( *ptr == NULL )
				return true;
		}
		
		return false;
	}

	// Parse
	bool XmlParser::ParseFile( RefWeak<IFile> file )
	{
		// parse root
		TRACE_DEBUG( _S("XmlParser - Parsing file <%s>.") , file->GetPath().c_str() );

		DataInfo info;
		file->GetInfo( info );

		// if file larger than 4k, use cached file object

		if( info.m_size >= 4096 )
		{
			// use 4k for file cache
			size_t buffersize = min<size_t>( (size_t)info.m_size , 4096 );
			// create file cache
			CachedFile cache( file , buffersize );
			return ParseDocument( cache );
		}
		else
		{
			byte * content = static_cast<byte*>( malloc( (size_t)( info.m_size + 1 ) ) );
			size_t read = file->Read( content , (size_t)info.m_size );
			content[ read ] = 0;
			bool result = ParseString( content );
			free( content );
			return result;
		}
	}

	// Parse
	bool XmlParser::ParseString( const byte * data )
	{
		return ParseDocument( data );
	}

} // namespace xeres
